package com.example.quartz.util;

import cn.hutool.cron.TaskExecutor;
import cn.hutool.cron.task.Task;
import com.example.common.constant.Constants;
import com.example.common.constant.ScheduleConstants;
import com.example.common.exception.job.TaskException;
import com.example.common.utils.StringUtils;
import com.example.common.utils.spring.SpringUtils;
import com.example.quartz.domain.SysJob;
import kotlin.sequences.TakeSequence;
import org.quartz.*;
import org.springframework.core.task.TaskRejectedException;

/**
 * 定时任务工具类
 * 提供Quartz定时任务相关的工具方法，如创建任务、获取任务键、设置任务策略等
 * Quartz定时任务主要包含以下核心组成部分：
 * 1. Job（任务）
 * Job 是定时任务的执行逻辑单元，代表需要执行的具体工作。
 * 2. Trigger（触发器）
 * Trigger 决定了任务何时执行、以何种频率执行，是任务调度的触发机制。
 * 3.Cron表达式
 * Cron表达式是一种用于定义定时任务执行时间的字符串表达式，决定了任务的执行计划。
 * 4. Scheduler（调度器）
 * Scheduler 是Quartz的核心组件，负责管理和协调Job与Trigger，是两者之间的桥梁。
 */
public class ScheduleUtils {

    /**
     * 根据系统任务配置获取对应的Quartz任务执行类
     * 根据任务是否允许并发执行，返回不同的任务执行类
     *
     * @param sysJob 系统任务对象，包含任务配置信息
     * @return 具体的Quartz任务执行类
     */
    private static Class<? extends Job> getQuartzJobClass(SysJob sysJob) {
        // 根据任务是否允许并发执行，返回不同的任务执行类, 0 表示不允许并发执行, 1 表示允许并发执行
        boolean isConcurrent = "0".equals(sysJob.getConcurrent());
        // 如果不允许并发，返回禁止并发执行的任务类；否则返回普通任务执行类
        return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
    }

    /**
     * 构建任务触发器的键对象
     * 触发器键由任务ID和任务组名组成，用于唯一标识一个触发器
     *
     * @param jobId    任务ID
     * @param jobGroup 任务组名
     * @return 触发器键对象
     */
    public static TriggerKey getTriggerKey(Long jobId, String jobGroup) {
        //使用任务类名前缀+任务ID作为触发器名称，结合任务组名创建触发器键
        return new TriggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
    }

    /**
     * 构建任务的键对象
     * 任务键有任务ID和任务组名组成，用于唯一标识一个任务
     *
     * @param jobId    任务ID
     * @param jobGroup 任务组名
     * @return 任务键对象
     */
    public static JobKey getJobKey(Long jobId, String jobGroup) {
        //使用任务类名前缀+任务ID作为任务名称，结合任务组名创建任务键
        return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
    }

    /**
     * 创建定时任务
     * 根据系统任务配置，在Quartz调度器中创建对应的定时任务
     * 1. 根据任务配置创建任务详情对象
     * 2. 根据任务配置创建Cron表达式
     * 3. 根据任务配置创建触发器对象
     * 4. 将任务详情和触发器注册到调度器
     * 5. 如果任务状态为暂停，则创建后立即暂停任务
     *
     * @param scheduler 任务调度器
     * @param job       系统任务对象，包含任务配置信息
     * @throws SchedulerException 任务调度器操作异常
     * @throws TaskException      任务配置异常
     */
    public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException {
        //获取适合当前任务的Quart任务执行类
        Class<? extends Job> jobClass = getQuartzJobClass(job);
        //获取任务ID和任务组名
        Long jobId = job.getJobId();
        String jobGroup = job.getJobGroup();
        //创建任务详情对象，设置任务类、任务ID、任务组名
        JobDetail jobDetail = JobBuilder.newJob(jobClass)
                .withIdentity(getJobKey(jobId, jobGroup))
                .build();
        //创建Cron表达式
        CronScheduleBuilder cronScheduleBuilder = org.quartz.CronScheduleBuilder.cronSchedule(job.getCronExpression());
        //根据任务的错过执行策略，设置调度器的处理方法
        cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);

        //创建触发器对象，设置触发器标识和调度规则
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(getTriggerKey(jobId, jobGroup))
                .withSchedule(cronScheduleBuilder)
                .build();

        //将系统任务对象存储到任务数据映射中，
        jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);

        //检查任务是否已经存在
        if (scheduler.checkExists(getJobKey(jobId, jobGroup))) {
            //如果任务已经存在，先删除再重新创建，避免数据问题
            scheduler.deleteJob(getJobKey(jobId, jobGroup));
        }

        //检查任务的cron表达式是否有效并且不过期
        if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) {
            //执行调度任务，将任务详情和触发器注册到调度器
            scheduler.scheduleJob(jobDetail, trigger);
        }

        //如果任务状态为暂停，则创建后立即暂停任务
        if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
            //暂停任务，使用任务键标识任务
            scheduler.pauseJob(getJobKey(jobId, jobGroup));
        }
    }

    /**
     * 设置定时任务的错过执行策略
     * 根据任务配置的错过执行策略，配置Cron调度器的处理方法
     *
     * @param job 系统任务对象
     * @param cb  Cron调度构建器
     * @return 配置了错过执行策略的Cron调度构建器
     * @throws TaskException 当任务配置了不支持的错过执行策略时抛出异常
     */
    public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb) throws TaskException {
        //根据任务配置的错过执行策略，选择对应的处理方式
        switch (job.getMisfirePolicy()) {
            case ScheduleConstants.MISFIRE_DEFAULT:
                return cb;
            case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
                //忽略所有错过的执行
                return cb.withMisfireHandlingInstructionIgnoreMisfires();
            case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
//                / 忽略所有错过的执行
                return cb.withMisfireHandlingInstructionFireAndProceed();
            case ScheduleConstants.MISFIRE_DO_NOTHING:
                // 立即触发一次执行，然后按计划继续
                return cb.withMisfireHandlingInstructionDoNothing();
            default:
                //如果配置了不支持的策略，抛出配置错误异常
                throw new TaskException("The task misfire policy '" + job.getMisfirePolicy() +
                        "' cannot be used in cron shedule tasks", TaskException.Code.CONFIG_ERROR);
        }
    }

    /**
     * 检查任务调用目标是否在白名单中
     * 用于安全控制，防止执行未授权的代码
     *
     * @param invokeTarget 任务调用目标字符串，格式为"类名.方法名(参数)"或"bean名称.方法名(参数)"
     * @return 如果在白名单中返回true，否则返回false
     */
    public static boolean whiteList(String invokeTarget) {
        //获取调用目标中的包名部分（括号前面的内容）
        String packageName = StringUtils.substringBefore(invokeTarget, "(");
        //统计包名中的点号数量
        int count = StringUtils.countMatches(packageName, ".");

        //如果点号数量大于1，说明是完整类名格式
        if (count > 1) {
            //检查完整类名是否包含在白名单字符串中
            return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR);
        }
        //如果点号数量不大于1，说明是bean名称.方法名格式
        //从spring容器中获取对应的bean
        Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]);
        //获取bean的实际包名
        String beanPackageName = obj.getClass().getPackage().getName();

        //这是？
        //检查bean的包名是否在白名单中并且不在黑名单中
        return StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_WHITELIST_STR)
                && !StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_ERROR_STR);
    }

}
